Skip to content

chore(lint): no single-character names, enforced and swept repo-wide - #129

Merged
jayleaton merged 10 commits into
mainfrom
chore/naming-strict
Oct 23, 2026
Merged

jayleaton merged 10 commits into
mainfrom
chore/naming-strict

Conversation

@jayleaton

@jayleaton jayleaton commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Rebuilt on main acb5cd1 after #117 merged (3e420ea): the renamer re-ran over everything, including the new storage, imaging and key-rotation code. That code (packages/storage, imaging) already used words, so nothing there needed renaming.

Problem

JJ's rule, final: "No single character names. It makes it far easier for a human to read and far easier for an agent to look up code that is well named." The template had 2,424 single-character declarations (func (s *Service), w http.ResponseWriter, t *testing.T, for i := range) and nothing stopped new ones.

Fix

Lint: scripts/namelint, run by make lint, make check-fast and CI (root module and deploy/). It's a ~60-line go/analysis checker: every identifier a package declares (types.Info.Defs) with a one-character name fails. That covers receivers, params, named results, locals, loop and closure variables, type-switch variables, type params, fields, labels and consts, in tests too. The message names the fix: single-character name "w": use a descriptive name: writer, request, test, index, service, caller (AGENTS.md "Taste"). Generated files (sqlc, mocks/) are skipped. *_templ.go is checked because templ copies the .templ source's Go verbatim (its own names are templ_7745c5c3_*), and the message says to fix it in the .templ file. There is no //nolint escape.

Why not a golangci-lint linter: I checked varnamelen (v0.8.0 in golangci v2.15.0). Its max-distance can't go below 1 (golangci drops 0), so for _, u := range users { use(u) } passes. It also hard-codes t *testing.T, b *testing.B, m *testing.M and similar as conventional, with no way to turn that off. forbidigo skips declarations and never sees a receiver used as s.field. No other linter checks name length. A golangci module plugin would mean building a custom golangci binary in CI.

Probes: make lint-layering plants the real past examples and requires a report for each, then requires the fixed form to pass:

ok    a single-character name fails namelint: func (s *Service)
ok    a single-character name fails namelint: w http.ResponseWriter
ok    a single-character name fails namelint: r *http.Request
ok    a single-character name fails namelint: for i := range
ok    a single-character name fails namelint: t *testing.T
ok    the fixed form (service, writer, request, index, test) passes namelint

Sweep: every one of the 2,424 declarations, and all their uses, was renamed with a type-aware renamer (scripts/namelint/rename, go/packages + go/types; gopls isn't installed here, and 2,400 single gopls rename calls with shifting offsets would be slow and fragile). It:

  • resolves each declaration and every use across packages and test variants;
  • names from type and context: writer/request/test/ctx for the stdlib types; service for a *Service receiver, consistent per type; user when ranging over users; left/right for comparator pairs; body for b := jsonBody(...); testCase for table rows;
  • refuses any name that is already declared in the scope, would be shadowed at a use, or would capture an outer name, so a refused name falls to the next candidate;
  • edits .templ sources through templ's source map (gofmt-aware: maps by token index), then make generate regenerates templ output and mocks.

The names it could not derive safely are hand-picked in 93 override lines in scripts/namelint/rename/overrides*.txt (for example Secrets receivers → accessors, i18n.T → i18n.Translate, JWK fields N/E/X/Y → Modulus/Exponent/CurveX/CurveY with their json tags unchanged, and receivers whose type name was taken).

group declarations renamed
packages/auth 901
apps/admin 507
apps/web 350
deploy 234
packages/secrets 167
backup (packages + apps) 126
packages/database 64
packages/uikit 39
scripts/testbudget 36
total 2,424

AGENTS.md "Taste": the "advice only" naming line is replaced by the strict rule, and the line says lint enforces it.

Behaviour unchanged (real output)

  • make check-fast (full run): check-fast: ok in 15.6s. golangci-lint 0 issues, namelint 0 in the root module and deploy/, vet clean.
  • Stripped binaries rebuilt before and after, then disassembled and compared with encodings, stack-slot offsets, data offsets and alignment NOPs ignored:
    • testbudget: 0 differing instructions.
    • railway-setup: only NOP padding, plus 8 lines where objdump disassembles embedded data.
    • server, admin, backup, preview: remaining differences are renamed symbols only: i18n.T → Translate, renamed struct fields inside generic shape and type:.eq names, and templ's Col: error metadata, which moves when a name changes length.
  • Same requests against the old and new web binaries (real Postgres, storage off: no S3-compatible image was pullable without a registry login): /healthz, sign-up, authenticated GET /users/me, anonymous 401, a validation error, the image route, the UI. With ids, tokens and timestamps masked, the responses are identical.
  • make lint-layering: 112 probes ok.
  • Full CI on Actions: see the manual ci.yml run on this branch.

Possible visible effect: a %+v print of a struct with a renamed unexported field shows the new field name. JSON output is unchanged: exported renames keep their tags, and unexported fields are never encoded.

Reproducing the sweep after a rebase

go run ./scripts/namelint ./...                      # what is left
go run ./scripts/namelint/rename -overrides scripts/namelint/rename/overrides.txt ./...          # the plan
go run ./scripts/namelint/rename -apply -overrides scripts/namelint/rename/overrides.txt ./...
(cd deploy && go run ../scripts/namelint/rename -apply -overrides ../scripts/namelint/rename/overrides-deploy.txt ./...)
gofmt -w apps packages scripts deploy && make generate && make check-fast && make lint-layering

New code merged after this sweep gets the same treatment. Positions in the overrides files go stale after a rebase. A stale override is harmless: its position no longer matches, and the renamer derives the name or prints SKIP with the reason. The renamer and its overrides are sweep tooling, and can be deleted in a follow-up once every branch has merged.

Merge risk

High churn (173 files), low behavioural risk: all renames are type-checked and the binaries match as described above. Every open branch that adds a single-character name fails lint after this merges, so it merges last, right after #117.

🤖 Generated with Claude Code

jayleaton and others added 10 commits October 23, 2026 18:36
…lint-layering; renamer for the sweep

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton force-pushed the chore/naming-strict branch from e484a3c to ad1e907 Compare October 23, 2026 11:36
@jayleaton
jayleaton changed the base branch from chore/naming-lint to main October 23, 2026 11:36
@jayleaton
jayleaton marked this pull request as ready for review October 23, 2026 11:37
@jayleaton
jayleaton merged commit 64f78a7 into main Oct 23, 2026
3 checks passed
jayleaton added a commit that referenced this pull request Oct 23, 2026
…enforcing (#131)

The sweep (#129) is merged and every branch has landed, so the renamer and
its overrides have nothing left to rename. New code is held to the rule by
scripts/namelint in make lint, make check-fast and CI.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant